package com.cloudhopper.commons.util.codec;

/*
 * #%L
 * ch-commons-util
 * %%
 * Copyright (C) 2012 Cloudhopper by Twitter
 * %%
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * #L%
 */

import java.nio.ByteBuffer;
import java.text.ParseException;

/**
 * Code originally copied from the OpenDS Java project.
 *
 * http://www.opends.org/
 *
 * This class removes the dependency on the Jakarta commons-codec library.
 * Since this utility package is used in every Cloudhopper Java project,
 * providing our own implementation (that's also much faster) allows the removal
 * of a transitive dependency for every Cloudhopper Java project :-)
 *
 * This class provides methods for performing base64 encoding and decoding.
 * Base64 is a mechanism for encoding binary data in ASCII form by converting
 * sets of three bytes with eight significant bits each to sets of four bytes
 * with six significant bits each.
 *
 * @author joelauer (twitter: @jjlauer or <a href="http://twitter.com/jjlauer" target=window>http://twitter.com/jjlauer</a>)
 */
public class Base64Codec {

    /**
     * The set of characters that may be used in base64-encoded values.
     */
    private static final char[] BASE64_ALPHABET =
            ("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" +
            "0123456789+/").toCharArray();

    /**
     * Prevent instance creation.
     */
    private Base64Codec() {
        // No implementation required.
    }

    /**
     * Encodes the provided raw data using base64.
     * @param  rawData  The raw data to encode.  It must not be <CODE>null</CODE>.
     * @return  The base64-encoded representation of the provided raw data.
     */
    public static String encode(byte[] rawData) {
        StringBuilder buffer = new StringBuilder(4 * rawData.length / 3);

        int pos = 0;
        int iterations = rawData.length / 3;
        for (int i = 0; i < iterations; i++) {
            int value = ((rawData[pos++] & 0xFF) << 16) |
                    ((rawData[pos++] & 0xFF) << 8) | (rawData[pos++] & 0xFF);

            buffer.append(BASE64_ALPHABET[(value >>> 18) & 0x3F]);
            buffer.append(BASE64_ALPHABET[(value >>> 12) & 0x3F]);
            buffer.append(BASE64_ALPHABET[(value >>> 6) & 0x3F]);
            buffer.append(BASE64_ALPHABET[value & 0x3F]);
        }

        switch (rawData.length % 3) {
            case 1:
                buffer.append(BASE64_ALPHABET[(rawData[pos] >>> 2) & 0x3F]);
                buffer.append(BASE64_ALPHABET[(rawData[pos] << 4) & 0x3F]);
                buffer.append("==");
                break;
            case 2:
                int value = ((rawData[pos++] & 0xFF) << 8) | (rawData[pos] & 0xFF);
                buffer.append(BASE64_ALPHABET[(value >>> 10) & 0x3F]);
                buffer.append(BASE64_ALPHABET[(value >>> 4) & 0x3F]);
                buffer.append(BASE64_ALPHABET[(value << 2) & 0x3F]);
                buffer.append("=");
                break;
        }

        return buffer.toString();
    }

    /**
     * Decodes the provided set of base64-encoded data.
     * @param  encodedData  The base64-encoded data to decode.  It must not be
     *                      <CODE>null</CODE>.
     * @return  The decoded raw data.
     * @throws  ParseException  If a problem occurs while attempting to decode the
     *                          provided data.
     */
    public static byte[] decode(String encodedData) throws ParseException {
        // The encoded value must have  length that is a multiple of four bytes.
        int length = encodedData.length();

        if ((length % 4) != 0) {
            //Message message = ERR_BASE64_DECODE_INVALID_LENGTH.get(encodedData);
            throw new ParseException("Base64 data was not 4-byte aligned", 0);
        }

        ByteBuffer buffer = ByteBuffer.allocate(length);
        for (int i = 0; i < length; i += 4) {
            boolean append = true;
            int value = 0;

            for (int j = 0; j < 4; j++) {
                switch (encodedData.charAt(i + j)) {
                    case 'A':
                        value <<= 6;
                        break;
                    case 'B':
                        value = (value << 6) | 0x01;
                        break;
                    case 'C':
                        value = (value << 6) | 0x02;
                        break;
                    case 'D':
                        value = (value << 6) | 0x03;
                        break;
                    case 'E':
                        value = (value << 6) | 0x04;
                        break;
                    case 'F':
                        value = (value << 6) | 0x05;
                        break;
                    case 'G':
                        value = (value << 6) | 0x06;
                        break;
                    case 'H':
                        value = (value << 6) | 0x07;
                        break;
                    case 'I':
                        value = (value << 6) | 0x08;
                        break;
                    case 'J':
                        value = (value << 6) | 0x09;
                        break;
                    case 'K':
                        value = (value << 6) | 0x0A;
                        break;
                    case 'L':
                        value = (value << 6) | 0x0B;
                        break;
                    case 'M':
                        value = (value << 6) | 0x0C;
                        break;
                    case 'N':
                        value = (value << 6) | 0x0D;
                        break;
                    case 'O':
                        value = (value << 6) | 0x0E;
                        break;
                    case 'P':
                        value = (value << 6) | 0x0F;
                        break;
                    case 'Q':
                        value = (value << 6) | 0x10;
                        break;
                    case 'R':
                        value = (value << 6) | 0x11;
                        break;
                    case 'S':
                        value = (value << 6) | 0x12;
                        break;
                    case 'T':
                        value = (value << 6) | 0x13;
                        break;
                    case 'U':
                        value = (value << 6) | 0x14;
                        break;
                    case 'V':
                        value = (value << 6) | 0x15;
                        break;
                    case 'W':
                        value = (value << 6) | 0x16;
                        break;
                    case 'X':
                        value = (value << 6) | 0x17;
                        break;
                    case 'Y':
                        value = (value << 6) | 0x18;
                        break;
                    case 'Z':
                        value = (value << 6) | 0x19;
                        break;
                    case 'a':
                        value = (value << 6) | 0x1A;
                        break;
                    case 'b':
                        value = (value << 6) | 0x1B;
                        break;
                    case 'c':
                        value = (value << 6) | 0x1C;
                        break;
                    case 'd':
                        value = (value << 6) | 0x1D;
                        break;
                    case 'e':
                        value = (value << 6) | 0x1E;
                        break;
                    case 'f':
                        value = (value << 6) | 0x1F;
                        break;
                    case 'g':
                        value = (value << 6) | 0x20;
                        break;
                    case 'h':
                        value = (value << 6) | 0x21;
                        break;
                    case 'i':
                        value = (value << 6) | 0x22;
                        break;
                    case 'j':
                        value = (value << 6) | 0x23;
                        break;
                    case 'k':
                        value = (value << 6) | 0x24;
                        break;
                    case 'l':
                        value = (value << 6) | 0x25;
                        break;
                    case 'm':
                        value = (value << 6) | 0x26;
                        break;
                    case 'n':
                        value = (value << 6) | 0x27;
                        break;
                    case 'o':
                        value = (value << 6) | 0x28;
                        break;
                    case 'p':
                        value = (value << 6) | 0x29;
                        break;
                    case 'q':
                        value = (value << 6) | 0x2A;
                        break;
                    case 'r':
                        value = (value << 6) | 0x2B;
                        break;
                    case 's':
                        value = (value << 6) | 0x2C;
                        break;
                    case 't':
                        value = (value << 6) | 0x2D;
                        break;
                    case 'u':
                        value = (value << 6) | 0x2E;
                        break;
                    case 'v':
                        value = (value << 6) | 0x2F;
                        break;
                    case 'w':
                        value = (value << 6) | 0x30;
                        break;
                    case 'x':
                        value = (value << 6) | 0x31;
                        break;
                    case 'y':
                        value = (value << 6) | 0x32;
                        break;
                    case 'z':
                        value = (value << 6) | 0x33;
                        break;
                    case '0':
                        value = (value << 6) | 0x34;
                        break;
                    case '1':
                        value = (value << 6) | 0x35;
                        break;
                    case '2':
                        value = (value << 6) | 0x36;
                        break;
                    case '3':
                        value = (value << 6) | 0x37;
                        break;
                    case '4':
                        value = (value << 6) | 0x38;
                        break;
                    case '5':
                        value = (value << 6) | 0x39;
                        break;
                    case '6':
                        value = (value << 6) | 0x3A;
                        break;
                    case '7':
                        value = (value << 6) | 0x3B;
                        break;
                    case '8':
                        value = (value << 6) | 0x3C;
                        break;
                    case '9':
                        value = (value << 6) | 0x3D;
                        break;
                    case '+':
                        value = (value << 6) | 0x3E;
                        break;
                    case '/':
                        value = (value << 6) | 0x3F;
                        break;
                    case '=':
                        append = false;
                        switch (j) {
                            case 2:
                                buffer.put((byte) ((value >>> 4) & 0xFF));
                                break;
                            case 3:
                                buffer.put((byte) ((value >>> 10) & 0xFF));
                                buffer.put((byte) ((value >>> 2) & 0xFF));
                                break;
                        }
                        break;
                    default:
                        //Message message = ERR_BASE64_DECODE_INVALID_CHARACTER.get(encodedData, encodedData.charAt(i + j));
                        throw new ParseException("Invalid Base64 character '" + encodedData.charAt(i + j) + "'", i+j);
                }

                if (!append) {
                    break;
                }
            }

            if (append) {
                buffer.put((byte) ((value >>> 16) & 0xFF));
                buffer.put((byte) ((value >>> 8) & 0xFF));
                buffer.put((byte) (value & 0xFF));
            } else {
                break;
            }
        }

        buffer.flip();
        byte[] returnArray = new byte[buffer.limit()];
        buffer.get(returnArray);
        return returnArray;
    }
}
